﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using EnvDTE;
using EnvDTE80;
using Calimbra.Addin.Messages;

namespace Calimbra.Addin.Functions
{
    class Create_file_infrastructure_code
    {
        private DTE2 dte;
        public void Process(List<FunctionUnit> function_units)
        {
            foreach (var functionUnit in function_units)
            {
                ProjectItem projectItem = dte.Solution.FindProjectItem(functionUnit.Name + ".cs");
                var fileCodeModel = (FileCodeModel2)projectItem.FileCodeModel;
                
                CreateUsings(fileCodeModel);
                CodeNamespace namespaceElement = CreateNamespace(projectItem, fileCodeModel);
                CodeClass2 classElement = CreateClass(functionUnit, namespaceElement);
                CreateConstructor(classElement);
                CreateContinueWithEvent(classElement);
                CreateProcessMethod(functionUnit.InputPortNames, classElement);
                CreateOutputWrappers(functionUnit.OutputPortNames, classElement);
                //CreateInputPorts(functionUnit.InputPortNames, classElement);
            }
        }

        private void CreateOutputWrappers(List<string> outputPortNames, CodeClass2 classElement)
        {
            foreach (var outputPortName in outputPortNames)
            {
                var method = (CodeFunction2)classElement.AddFunction(string.Concat("Fire",outputPortName), vsCMFunction.vsCMFunctionFunction,
                                                                        vsCMTypeRef.vsCMTypeRefVoid, -1,
                                                                        vsCMAccess.vsCMAccessPrivate);
                method.AddParameter("outputMessage", "IMessage", 1);
                var startPoint = (EditPoint2)method.GetStartPoint(vsCMPart.vsCMPartBody).CreateEditPoint();
                var editPoint = (EditPoint2)method.GetStartPoint(vsCMPart.vsCMPartBody).CreateEditPoint();
                editPoint.Insert(string.Format("outputMessage.Output.Port.Name = base.Name + \".{0}\";", outputPortName));
                editPoint.InsertNewLine();
                editPoint.Insert("ContinueWith(outputMessage);");
                startPoint.Indent(editPoint, startPoint.DisplayColumn + 2);
            }
        }

        private void CreateProcessMethod(List<string> inputPortNames, CodeClass2 classElement)
        {
            var method = (CodeFunction2)classElement.AddFunction("Process", vsCMFunction.vsCMFunctionFunction,
                                                                         vsCMTypeRef.vsCMTypeRefVoid, -1,
                                                                         vsCMAccess.vsCMAccessPublic);
            method.AddParameter("message", "IMessage",1);
            method.AddParameter("continueWith","Action<IMessage>",2);

            var startPoint = (EditPoint2)method.GetStartPoint(vsCMPart.vsCMPartBody).CreateEditPoint();
            var editPoint = (EditPoint2)method.GetStartPoint(vsCMPart.vsCMPartBody).CreateEditPoint();
            editPoint.Insert("ContinueWith = continueWith;\n");
            editPoint.Insert("switch (message.Port.Name.ToLower())\n{\n");

            CreateCaseStatements(editPoint, inputPortNames);

            editPoint.Insert("}");
            startPoint.Indent(editPoint, startPoint.DisplayColumn + 2);
            
        
        }

        private void CreateCaseStatements(EditPoint2 editPoint, List<string> inputPortNames)
        {
            var startPoint = (EditPoint2) editPoint.CreateEditPoint();
            foreach (var inputPortName in inputPortNames)
            {
                editPoint.Insert(string.Format("case \"{0}\":\n", inputPortName.ToLower()));
                CreateCaseContent(editPoint, inputPortName);             
            }
            editPoint.Insert("default:\n");
            CreateCaseDefaultContent(editPoint);
            startPoint.Indent(editPoint, 1);
        }

        private void CreateCaseContent(EditPoint2 editPoint, string inputPortName)
        {
            var startPoint = (EditPoint2)editPoint.CreateEditPoint();
            editPoint.Insert(string.Format("throw new NotImplementedException(\"Cast message.Data (var inputPort_{0}_Message = (CASTHERE)message.Data) to use it in your operation and Fire[OutputPort](new Message(YOURRESULTDATA)).\");\nbreak;\n", inputPortName));
            startPoint.Indent(editPoint, 1);
        }

        private void CreateCaseDefaultContent(EditPoint2 editPoint)
        {
            var startPoint = (EditPoint2)editPoint.CreateEditPoint();
            editPoint.Insert("throw new ArgumentException(\"Ungültiger Input-Port: \" + message.Port.Name);");
            startPoint.Indent(editPoint, 1);
        }
        private void CreateUsings(FileCodeModel2 codeModel)
        {
            var usings = new List<string>();
            foreach (CodeElement2 codeElement in codeModel.CodeElements)
            {
                if (codeElement.Kind == vsCMElement.vsCMElementImportStmt)
                {
                    var importElement = (CodeImport) codeElement;
                    usings.Add(importElement.Namespace);
                }
            }
            if (!usings.Contains("System"))
                codeModel.AddImport("System");
            if (!usings.Contains("Calimbra.Runtime.Contracts"))
                codeModel.AddImport("Calimbra.Runtime.Contracts");    
        }

        private CodeNamespace CreateNamespace(ProjectItem projectItem, FileCodeModel2 codeModel)
        {
            CodeNamespace namespaceElement = null;
            foreach (CodeElement2 codeElement in codeModel.CodeElements)
            {
                if (codeElement.Kind == vsCMElement.vsCMElementNamespace)
                    namespaceElement = (CodeNamespace) codeElement;
            }
            if (namespaceElement == null)
            {
                namespaceElement = codeModel.AddNamespace(projectItem.ContainingProject.Properties.Item("DefaultNamespace").Value.ToString(), -1);
            }
            return namespaceElement;
        }

        private CodeClass2 CreateClass(FunctionUnit functionUnit, CodeNamespace namespaceElement)
        {
            CodeClass2 classElement = null;
            foreach (CodeElement2 codeElement in namespaceElement.Children)
            {
                if (codeElement.Kind == vsCMElement.vsCMElementClass)
                {
                    if (codeElement.Name == functionUnit.Name)
                        classElement = (CodeClass2) codeElement;
                }
            }
            if (classElement == null)
            {
                classElement = (CodeClass2)namespaceElement.AddClass(functionUnit.Name, -1, "BaseOperation", null, vsCMAccess.vsCMAccessPublic);
            }
            else
            {
                // TODO: Prüfe ob von BaseOperation abgeleitet und leite ggf ab
            }
            return classElement;
        }

        private void CreateConstructor(CodeClass2 classElement)
        {
            bool ctorExists = false;
            foreach (CodeElement2 codeElement in classElement.Children)
            {
                if (codeElement.Kind == vsCMElement.vsCMElementFunction)
                {
                    var functionElement = (CodeFunction)codeElement;
                    if (functionElement.FunctionKind == vsCMFunction.vsCMFunctionConstructor)
                    {
                        ctorExists = true;
                    }
                }
            }
            if (!ctorExists)
            {
                var constructor = (CodeFunction2)classElement.AddFunction(classElement.Name, vsCMFunction.vsCMFunctionConstructor,
                                                                           vsCMTypeRef.vsCMTypeRefOther, -1,
                                                                           vsCMAccess.vsCMAccessPublic);
                constructor.AddParameter("name", "string");
                var point = (EditPoint2)constructor.GetStartPoint(vsCMPart.vsCMPartHeader).CreateEditPoint();
                string s = point.GetText(constructor.GetStartPoint(vsCMPart.vsCMPartBody));
                point.CharRight(s.IndexOf(')') + 1);
                point.Insert(" : base(name)");
            }
        }

        private void CreateContinueWithEvent(CodeClass2 classElement)
        {
            bool eventExists = false;
            foreach (CodeElement2 codeElement in classElement.Children)
            {
                if (codeElement.Kind == vsCMElement.vsCMElementEvent)
                {
                    if (codeElement.Name == "ContinueWith")
                    {
                        eventExists = true;
                        break;
                    }
                }
            }
            if (!eventExists)
            {
                classElement.AddEvent("ContinueWith", "Action<IMessage>", false, -1, vsCMAccess.vsCMAccessPrivate);
            }
        }



        private void CreateInputPorts(List<string> inputPortNames, CodeClass2 classElement)
        {
            var existingMethods = new List<string>();
            foreach (CodeElement2 codeElement in classElement.Children)
            {
                if (codeElement.Kind == vsCMElement.vsCMElementFunction)
                {
                    if (inputPortNames.Contains(codeElement.Name))
                        existingMethods.Add(codeElement.Name);
                }
            }


            foreach (string inputPortName in inputPortNames)
            {
                if (!existingMethods.Contains(inputPortName))
                {
                    var method = (CodeFunction2)classElement.AddFunction(inputPortName, vsCMFunction.vsCMFunctionFunction,
                                                                          vsCMTypeRef.vsCMTypeRefVoid, -1,
                                                                          vsCMAccess.vsCMAccessPublic);
                    method.AddParameter("message", "IMessage");
                }
            }
        }

        public void Inject(DTE2 dte2)
        {
            dte = dte2;
        }
    }
}
